{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Type Testing (type, isinstance) [Pre-reading]\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class A: pass\n",
    "a = A()\n",
    "print(type(a))           # A (technically, < class '__main__.A' >)\n",
    "print(type(a) == A)      # True\n",
    "print(isinstance(a, A))  # True"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Special Methods\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Equality Testing (__eq__) [Pre-reading]\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The problem: Shouldn't a1 == a2?\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class A:\n",
    "    def __init__(self, x):\n",
    "        self.x = x\n",
    "a1 = A(5)\n",
    "a2 = A(5)\n",
    "print(a1 == a2)  # False!"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The partial solution: __eq__\n",
    "The __eq__ method tells Python how to evalute the equality of two objects."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class A:\n",
    "    def __init__(self, x):\n",
    "        self.x = x\n",
    "    def __eq__(self, other):\n",
    "        return (self.x == other.x)\n",
    "a1 = A(5)\n",
    "a2 = A(5)\n",
    "print(a1 == a2)  # True\n",
    "print(a1 == 99)  # crash (darn!)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "A better solution:\n",
    "Here we don't crash on unexpected types of other:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class A:\n",
    "    def __init__(self, x):\n",
    "        self.x = x\n",
    "    def __eq__(self, other):\n",
    "        return (isinstance(other, A) and (self.x == other.x))\n",
    "a1 = A(5)\n",
    "a2 = A(5)\n",
    "print(a1 == a2)  # True\n",
    "print(a1 == 99)  # False (huzzah!)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Converting to Strings (__str__ and __repr__) [Pre-reading]\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The problem:\n",
    "Just like with ==, Python doesn't really know how to print our objects..."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class A:\n",
    "    def __init__(self, x):\n",
    "        self.x = x\n",
    "a = A(5)\n",
    "print(a) # prints <__main__.A object at 0x102916128> (yuck!)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\n",
    "The partial solution: __str__\n",
    "The __str__ method tells Python how to convert the object to a string, but it is not used in some cases (such as when the object is in a list):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class A:\n",
    "    def __init__(self, x):\n",
    "        self.x = x\n",
    "    def __str__(self):\n",
    "        return f'A(x={self.x})'\n",
    "a = A(5)\n",
    "print(a) # prints A(x=5) (better)\n",
    "print([a]) # prints [<__main__.A object at 0x102136278>] (yuck!)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The better solution: __repr__\n",
    "The __repr__ method is used inside lists (and other places):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Note: repr should be a computer-readable form so that\n",
    "# (eval(repr(obj)) == obj), but we are not using it that way.\n",
    "# So this is a simplified use of repr.\n",
    "\n",
    "class A:\n",
    "    def __init__(self, x):\n",
    "        self.x = x\n",
    "    def __repr__(self):\n",
    "        return f'A(x={self.x})'\n",
    "a = A(5)\n",
    "print(a) # prints A(x=5) (better)\n",
    "print([a]) # [A(x=5)]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Using in Sets and Dictionaries (__hash__ and __eq__)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The problem:\n",
    "Objects do not seem to hash right by default:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class A:\n",
    "    def __init__(self, x):\n",
    "        self.x = x\n",
    "\n",
    "a = A(5)\n",
    "b = A(5)\n",
    "\n",
    "print(hash(a) == hash(b))       # False (this is surprising)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "And that produces this unfortunate situation:\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class A:\n",
    "    def __init__(self, x):\n",
    "        self.x = x\n",
    "\n",
    "s = set()\n",
    "s.add(A(5))\n",
    "print(A(5) in s) # False\n",
    "\n",
    "d = dict()\n",
    "d[A(5)] = 42\n",
    "print(d[A(5)]) # crashes"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The solution: __hash__ and __eq__\n",
    "The __hash__ method tells Python how to hash the object. The properties you choose to hash on should be immutable types and should never change (so hash(obj) is immutable)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class A:\n",
    "    def __init__(self, x):\n",
    "        self.x = x\n",
    "    def __hash__(self):\n",
    "        return hash(self.x)\n",
    "    def __eq__(self, other):\n",
    "        return (isinstance(other, A) and (self.x == other.x))\n",
    "\n",
    "s = set()\n",
    "s.add(A(5))\n",
    "print(A(5) in s) # True (whew!)\n",
    "\n",
    "d = dict()\n",
    "d[A(5)] = 42\n",
    "print(d[A(5)]) # works!"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "A better (more generalizable) solution\n",
    "You can define the method getHashables that packages the things you want to hash into a tuple, and then you can use a more generic approach to __hash__:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Your getHashables method should return the values upon which\n",
    "# your hash method depends, that is, the values that your __eq__\n",
    "# method requires to test for equality.\n",
    "# CAVEAT: a proper hash function should only test values that will not change!\n",
    "\n",
    "class A:\n",
    "    def __init__(self, x):\n",
    "        self.x = x\n",
    "    def getHashables(self):\n",
    "        return (self.x, ) # return a tuple of hashables\n",
    "    def __hash__(self):\n",
    "        return hash(self.getHashables())\n",
    "    def __eq__(self, other):\n",
    "        return (isinstance(other, A) and (self.x == other.x))\n",
    "\n",
    "s = set()\n",
    "s.add(A(5))\n",
    "print(A(5) in s) # True (still works!)\n",
    "\n",
    "d = dict()\n",
    "d[A(5)] = 42\n",
    "print(d[A(5)]) # works!"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Fraction Example\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Very simple, partly-implemented Fraction class\n",
    "# to demonstrate the OOP ideas from above.\n",
    "# Note that Python actually has a full Fraction class that\n",
    "# you would use instead (from fractions import Fraction),\n",
    "# so this is purely for demonstrational purposes.\n",
    "\n",
    "def gcd(x, y):\n",
    "    if (y == 0): return x\n",
    "    else: return gcd(y, x%y)\n",
    "\n",
    "class Fraction:\n",
    "    def __init__(self, num, den):\n",
    "        # Partial implementation -- does not deal with 0 or negatives, etc\n",
    "        g = gcd(num, den)\n",
    "        self.num = num // g\n",
    "        self.den = den // g\n",
    "\n",
    "    def __repr__(self):\n",
    "        return '%d/%d' % (self.num, self.den)\n",
    "\n",
    "    def __eq__(self, other):\n",
    "        return (isinstance(other, Fraction) and\n",
    "                ((self.num == other.num) and (self.den == other.den)))\n",
    "\n",
    "    def times(self, other):\n",
    "        if (isinstance(other, int)):\n",
    "            return Fraction(self.num * other, self.den)\n",
    "        else:\n",
    "            return Fraction(self.num * other.num, self.den * other.den)\n",
    "\n",
    "    def __hash__(self):\n",
    "        return hash((self.num, self.den))\n",
    "\n",
    "def testFractionClass():\n",
    "    print('Testing Fraction class...', end='')\n",
    "    assert(str(Fraction(2, 3)) == '2/3')\n",
    "    assert(str([Fraction(2, 3)]) == '[2/3]')\n",
    "    assert(Fraction(2,3) == Fraction(2,3))\n",
    "    assert(Fraction(2,3) != Fraction(2,5))\n",
    "    assert(Fraction(2,3) != \"Don't crash here!\")\n",
    "    assert(Fraction(2,3).times(Fraction(3,4)) == Fraction(1,2))\n",
    "    assert(Fraction(2,3).times(5) == Fraction(10,3))\n",
    "    s = set()\n",
    "    assert(Fraction(1, 2) not in s)\n",
    "    s.add(Fraction(1, 2))\n",
    "    assert(Fraction(1, 2) in s)\n",
    "    s.remove(Fraction(1, 2))\n",
    "    assert(Fraction(1, 2) not in s)\n",
    "    print('Passed.')\n",
    "\n",
    "if (__name__ == '__main__'):\n",
    "    testFractionClass()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Class-Level Features\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Class Attributes\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Class Attributes are values specified in a class that are shared by all instances of that class! We can access class attributes from any instance of that class, but changing those values anywhere changes them for every instance.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class A:\n",
    "    dirs = [\"up\", \"down\", \"left\", \"right\"]\n",
    "\n",
    "# typically access class attributes directly via the class (no instance!)\n",
    "print(A.dirs) # ['up', 'down', 'left', 'right']\n",
    "\n",
    "# can also access via an instance:\n",
    "a = A()\n",
    "print(a.dirs)\n",
    "\n",
    "# but there is only one shared value across all instances:\n",
    "a1 = A()\n",
    "a1.dirs.pop() # not a good idea\n",
    "a2 = A()\n",
    "print(a2.dirs) # ['up', 'down', 'left'] ('right' is gone from A.dirs)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Static Methods\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Static Methods in a class can be called directly without making or referencing a specific object.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class A:\n",
    "    @staticmethod\n",
    "    def f(x):\n",
    "        return 10*x\n",
    "\n",
    "print(A.f(42)) # 420 (called A.f without creating an instance of A)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Playing Card Demo\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# oopy-playing-cards-demo.py\n",
    "# Demos class attributes, static methods, repr, eq, hash\n",
    "\n",
    "import random\n",
    "\n",
    "class PlayingCard:\n",
    "    numberNames = [None, \"Ace\", \"2\", \"3\", \"4\", \"5\", \"6\", \"7\",\n",
    "                   \"8\", \"9\", \"10\", \"Jack\", \"Queen\", \"King\"]\n",
    "    suitNames = [\"Clubs\", \"Diamonds\", \"Hearts\", \"Spades\"]\n",
    "    CLUBS = 0\n",
    "    DIAMONDS = 1\n",
    "    HEARTS = 2\n",
    "    SPADES = 3\n",
    "\n",
    "    @staticmethod\n",
    "    def getDeck(shuffled=True):\n",
    "        deck = [ ]\n",
    "        for number in range(1, 14):\n",
    "            for suit in range(4):\n",
    "                deck.append(PlayingCard(number, suit))\n",
    "        if (shuffled):\n",
    "            random.shuffle(deck)\n",
    "        return deck\n",
    "\n",
    "    def __init__(self, number, suit):\n",
    "        # number is 1 for Ace, 2...10,\n",
    "        #           11 for Jack, 12 for Queen, 13 for King\n",
    "        # suit is 0 for Clubs, 1 for Diamonds,\n",
    "        #         2 for Hearts, 3 for Spades\n",
    "        self.number = number\n",
    "        self.suit = suit\n",
    "\n",
    "    def __repr__(self):\n",
    "        number = PlayingCard.numberNames[self.number]\n",
    "        suit = PlayingCard.suitNames[self.suit]\n",
    "        return f'<{number} of {suit}>'\n",
    "\n",
    "    def getHashables(self):\n",
    "        return (self.number, self.suit) # return a tuple of hashables\n",
    "\n",
    "    def __hash__(self):\n",
    "        # you are not responsible for this method\n",
    "        return hash(self.getHashables())\n",
    "\n",
    "    def __eq__(self, other):\n",
    "        return (isinstance(other, PlayingCard) and\n",
    "                (self.number == other.number) and\n",
    "                (self.suit == other.suit))\n",
    "\n",
    "# Show this code in action\n",
    "print(\"Demo of PlayingCard will keep creating new decks, and\")\n",
    "print(\"drawing the first card, until we see the same card twice.\")\n",
    "print()\n",
    "cardsSeen = set()\n",
    "diamondsCount = 0\n",
    "\n",
    "# Now keep drawing cards until we get a duplicate\n",
    "while True:\n",
    "    deck = PlayingCard.getDeck()\n",
    "    drawnCard = deck[0]\n",
    "    if (drawnCard.suit == PlayingCard.DIAMONDS):\n",
    "        diamondsCount += 1\n",
    "    print(\"  drawnCard:\", drawnCard)\n",
    "    if (drawnCard in cardsSeen): break\n",
    "    cardsSeen.add(drawnCard)\n",
    "\n",
    "# And then report how many cards we drew\n",
    "print(\"Total cards drawn:\", 1+len(cardsSeen))\n",
    "print(\"Total diamonds drawn:\", diamondsCount)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Inheritance [Pre-reading]\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "A subclass inherits all the methods from its superclass, and then can add or modify methods.\n"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Specifying a Superclass [Pre-reading]\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class A:\n",
    "    def __init__(self, x):\n",
    "        self.x = x\n",
    "    def f(self):\n",
    "        return 10*self.x\n",
    "\n",
    "class B(A):\n",
    "    def g(self):\n",
    "        return 1000*self.x\n",
    "\n",
    "print(A(5).f()) # 50\n",
    "print(B(7).g()) # 7000\n",
    "print(B(7).f()) # 70 (class B inherits the method f from class A)\n",
    "print(A(5).g()) # crashes (class A does not have a method g)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Overriding methods [Pre-reading]\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can change a method's behavior in a subclass by overriding it.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class A:\n",
    "    def __init__(self, x):\n",
    "        self.x = x\n",
    "    def f(self):\n",
    "        return 10*self.x\n",
    "    def g(self):\n",
    "        return 100*self.x\n",
    "\n",
    "class B(A):\n",
    "    def __init__(self, x=42, y=99):\n",
    "        super().__init__(x) # call overridden init!\n",
    "        self.y = y\n",
    "    def f(self):\n",
    "        return 1000*self.x\n",
    "    def g(self):\n",
    "        return (super().g(), self.y)\n",
    "\n",
    "a = A(5)\n",
    "b = B(7)\n",
    "print(a.f()) # 50\n",
    "print(a.g()) # 500\n",
    "print(b.f()) # 7000\n",
    "print(b.g()) # (700, 99)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## isinstance vs type in inherited classes [Pre-reading]\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class A: pass\n",
    "class B(A): pass\n",
    "a = A()\n",
    "b = B()\n",
    "print(type(a) == A) # True\n",
    "print(type(b) == A) # False\n",
    "print(type(a) == B) # False\n",
    "print(type(b) == B) # True\n",
    "print()\n",
    "print(isinstance(a, A)) # True\n",
    "print(isinstance(b, A)) # True (surprised?)\n",
    "print(isinstance(a, B)) # False\n",
    "print(isinstance(b, B)) # True"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Monster Demo [Pre-reading]\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# This is our base class\n",
    "class Monster:\n",
    "    def __init__(self, strength, defense):\n",
    "        self.strength = strength\n",
    "        self.defense = defense\n",
    "        self.health = 10\n",
    "\n",
    "    def attack(self): # returns damage to be dealt\n",
    "        if self.health > 0:\n",
    "            return self.strength\n",
    "\n",
    "    def defend(self, damage): # does damage to self\n",
    "        self.health -= damage\n",
    "\n",
    "class MagicMonster(Monster):\n",
    "    def __init__(self, strength, defense):\n",
    "        super().__init__(strength, defense) # most properties are the same\n",
    "        self.health = 5 # but they start out weaker\n",
    "\n",
    "    def heal(self): # only magic monsters can heal themselves!\n",
    "        if 0 < self.health < 5:\n",
    "            self.health += 1\n",
    "\n",
    "class NecroMonster(Monster):\n",
    "    def attack(self): # NecroMonsters can attack even when 'killed'\n",
    "        return self.strength"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Additional Reading\n"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "For more on these topics, and many additional OOP-related topics, check the following links:\n",
    "     https://docs.python.org/3/tutorial/classes.html\n",
    "     https://docs.python.org/3/reference/datamodel.html\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "p2s",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.9.15"
  },
  "orig_nbformat": 4
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
